package domain

import (
	"context"
	"time"

	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/backend/v3/storage/database"
)

//go:generate mockgen -typed -package domainmock -destination ./mock/user.mock.go . UserRepository

type UserRepository interface {
	Repository
	Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*User, error)
	List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*User, error)
	Create(ctx context.Context, client database.QueryExecutor, user *User) error
	Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
	Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)

	Human() HumanUserRepository
	Machine() MachineUserRepository

	LoadMetadata() UserRepository
	LoadPasskeys() UserRepository
	LoadIdentityProviderLinks() UserRepository
	LoadVerifications() UserRepository
	LoadPATs() UserRepository
	LoadKeys() UserRepository

	userConditions
	userChanges
	userColumns
}

type userConditions interface {
	PrimaryKeyCondition(instanceID, userID string) database.Condition
	InstanceIDCondition(instanceID string) database.Condition
	OrgIDCondition(orgID string) database.Condition
	IDCondition(userID string) database.Condition
	UsernameCondition(op database.TextOperation, username string) database.Condition
	StateCondition(state UserState) database.Condition
	TypeCondition(userType UserType) database.Condition
	LoginNameCondition(op database.TextOperation, loginName string) database.Condition

	userMetadataConditions
}

type userMetadataConditions interface {
	ExistsMetadata(condition database.Condition) database.Condition
	MetadataKeyCondition(op database.TextOperation, key string) database.Condition
	MetadataValueCondition(op database.BytesOperation, value []byte) database.Condition
}

type userChanges interface {
	// SetUsername sets the username field
	SetUsername(username string) database.Change
	// SetState sets the state field
	// [UserStateUnspecified] is not allowed and will result in no change
	SetState(state UserState) database.Change
	// SetUpdatedAt sets the updated at field
	// This is used to replay events
	SetUpdatedAt(updatedAt time.Time) database.Change
	// AddMetadata adds metadata for the user
	AddMetadata(metadata ...*Metadata) database.Change
	// RemoveMetadata removes the metadata for the user
	// Use one of the following conditions to specify which metadata to remove:
	//  - [userMetadataConditions.MetadataKeyCondition]
	//  - [userMetadataConditions.MetadataValueCondition]
	//  - nil to remove all metadata for the user
	RemoveMetadata(condition database.Condition) database.Change
}

type userColumns interface {
	PrimaryKeyColumns() []database.Column
	UsernameColumn() database.Column
	StateColumn() database.Column
	TypeColumn() database.Column
	CreatedAtColumn() database.Column
}

//go:generate mockgen -typed -package domainmock -destination ./mock/human_user.mock.go . HumanUserRepository

type HumanUserRepository interface {
	// Update updates the human user
	// It ensures that updates are only applied to human users
	Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
	humanConditions
	humanChanges
	humanColumns
}

type humanConditions interface {
	userConditions
	FirstNameCondition(op database.TextOperation, firstName string) database.Condition
	LastNameCondition(op database.TextOperation, lastName string) database.Condition
	NicknameCondition(op database.TextOperation, nickname string) database.Condition
	DisplayNameCondition(op database.TextOperation, displayName string) database.Condition
	PreferredLanguageCondition(lang language.Tag) database.Condition
	EmailCondition(op database.TextOperation, email string) database.Condition
	PhoneCondition(op database.TextOperation, phone string) database.Condition
	PasskeyIDCondition(passkeyID string) database.Condition
	IdentityProviderIDCondition(idpID string) database.Condition
	ProvidedUserIDCondition(providedUserID string) database.Condition
	ProvidedUsernameCondition(username string) database.Condition

	humanPasskeyConditions
}

type humanChanges interface {
	userChanges
	// SetFirstName sets the first name field
	SetFirstName(firstName string) database.Change
	// SetLastName sets the last name field
	SetLastName(lastName string) database.Change
	// SetNickname sets the nickname field
	SetNickname(nickname string) database.Change
	// SetDisplayName sets the display name field
	SetDisplayName(displayName string) database.Change
	// SetPreferredLanguage sets the preferred language field
	// If preferredLanguage is [language.Und], it will be set to NULL in the database
	SetPreferredLanguage(preferredLanguage language.Tag) database.Change
	// SetGender sets the gender field
	// If gender is [HumanGenderUnspecified], it will be set to NULL in the database
	SetGender(gender HumanGender) database.Change
	// SetAvatarKey sets the avatar key field
	// If avatarKey is nil, it will be set to NULL in the database
	SetAvatarKey(avatarKey *string) database.Change
	// SetMultifactorInitializationSkippedAt sets the multifactor initialization skipped at field
	SetMultifactorInitializationSkippedAt(skippedAt time.Time) database.Change

	// SetPassword sets the password based on the verification
	SetPassword(verification VerificationType) database.Change
	// SetPasswordChangeRequired sets whether a password change is required
	SetPasswordChangeRequired(required bool) database.Change
	// CheckPassword sets the password check based on the check type
	//  * [CheckTypeFailed] increments failed attempts
	//  * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
	CheckPassword(check PasswordCheckType) database.Change

	// SetVerification sets the verification based on the verification type
	SetVerification(id string, verification VerificationType) database.Change

	humanEmailChanges
	humanPhoneChanges
	humanTOTPChanges
	identityProviderLinkChanges
	humanPasskeyChanges
}

type humanEmailChanges interface {
	// SetEmail sets the email based on the verification
	// 	* [VerificationTypeInit] to initialize email verification, previously verified email remains verified
	// 	* [VerificationTypeVerified] to mark email as verified, a verification must exist
	// 	* [VerificationTypeUpdate] to update email verification, a verification must exist (e.g. resend code)
	// 	* [VerificationTypeSkipped] to skip email verification, existing verification is removed (e.g. admin set email)
	SetEmail(verification VerificationType) database.Change
	// CheckEmailOTP sets the email OTP based on the check
	//  * [CheckTypeInit] to initialize a new check, previous check is overwritten
	//  * [CheckTypeFailed] increments failed attempts
	//  * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
	CheckEmailOTP(check CheckType) database.Change
	// EnableEmailOTPAt enables the email OTP
	// If enabledAt is zero, it will be set to NOW()
	EnableEmailOTPAt(enabledAt time.Time) database.Change
	// EnableEmailOTP sets the enabled at time to NOW()
	EnableEmailOTP() database.Change
	// DisableEmailOTP clears the enabled at time
	DisableEmailOTP() database.Change
}

type humanPhoneChanges interface {
	// SetPhone sets the phone based on the verification
	// 	* [VerificationTypeInit] to initialize phone verification, previously verified phone remains verified
	// 	* [VerificationTypeVerified] to mark phone as verified, a verification must exist
	// 	* [VerificationTypeUpdate] to update phone verification, a verification must exist (e.g. resend code)
	// 	* [VerificationTypeSkipped] to skip phone verification, existing verification is removed (e.g. admin set phone)
	SetPhone(verification VerificationType) database.Change
	// RemovePhone removes the phone number
	RemovePhone() database.Change
	// CheckSMSOTP sets the SMS OTP based on the check
	//  * [CheckTypeInit] to initialize a new check, previous check is overwritten
	//  * [CheckTypeFailed] increments failed attempts
	//  * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
	CheckSMSOTP(check CheckType) database.Change
	// EnableSMSOTPAt enables the SMS OTP
	// If enabledAt is zero, it will be set to NOW()
	EnableSMSOTPAt(enabledAt time.Time) database.Change
	// EnableSMSOTP sets the enabled at time to NOW()
	EnableSMSOTP() database.Change
	// DisableSMSOTP clears the enabled at time
	DisableSMSOTP() database.Change
}

type humanTOTPChanges interface {
	// SetTOTP changes the TOTP secret based on the verification
	// 	* [VerificationTypeInit] to initialize totp verification, previously verified totp remains verified
	// 	* [VerificationTypeVerified] to mark totp as verified, a verification must exist
	// 	* [VerificationTypeUpdate] to update totp verification, a verification must exist (e.g. resend code)
	// 	* [VerificationTypeSkipped] to skip totp verification, existing verification is removed (e.g. admin set totp)
	SetTOTP(verification VerificationType) database.Change
	// SetTOTPCheck sets the TOTP check based on the check type
	//  * [CheckTypeFailed] increments failed attempts
	//  * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
	//  * [CheckTypeInit] is not allowed for TOTP
	CheckTOTP(check CheckType) database.Change
	// RemoveTOTP removes the TOTP
	RemoveTOTP() database.Change
}

type identityProviderLinkChanges interface {
	// AddIdentityProviderLink adds or updates an identity provider link
	AddIdentityProviderLink(link *IdentityProviderLink) database.Change
	UpdateIdentityProviderLink(changes ...database.Change) database.Change
	// RemoveIdentityProviderLink removes an identity provider link based on the condition
	RemoveIdentityProviderLink(providerID, providedUserID string) database.Change

	// SetIdentityProviderLinkUsername sets the username for an identity provider link
	SetIdentityProviderLinkUsername(providerID, providedUserID, username string) database.Change
	// SetIdentityProviderLinkProvidedID sets the provided user ID for an identity provider link
	SetIdentityProviderLinkProvidedID(providerID, currentProvidedUserID, newProvidedUserID string) database.Change
}

type humanColumns interface {
	userColumns
	FirstNameColumn() database.Column
	LastNameColumn() database.Column
	NicknameColumn() database.Column
	DisplayNameColumn() database.Column
	EmailColumn() database.Column
}

//go:generate mockgen -typed -package domainmock -destination ./mock/machine_user.mock.go . MachineUserRepository

type MachineUserRepository interface {
	// Update updates the machine user
	// It ensures that updates are only applied to machine users
	Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
	machineConditions
	machineChanges
	machineColumns
}

type machineConditions interface {
	userConditions
}

type machineChanges interface {
	userChanges
	// SetName sets the name field
	SetName(name string) database.Change
	// SetDescription sets the description field
	SetDescription(description string) database.Change
	// SetSecret sets the secret field
	// nil will set the secret to NULL in the database
	SetSecret(secret *string) database.Change
	// SetAccessTokenType sets the personal access token type field
	SetAccessTokenType(tokenType PersonalAccessTokenType) database.Change

	// AddKey adds a key for the machine user
	AddKey(key *MachineKey) database.Change
	// RemoveKey removes a key for the machine user
	RemoveKey(id string) database.Change

	// AddPersonalAccessToken adds a personal access token for the machine user
	AddPersonalAccessToken(pat *PersonalAccessToken) database.Change
	// RemovePersonalAccessToken removes a personal access token for the machine user
	RemovePersonalAccessToken(id string) database.Change
}

type machineColumns interface {
	userColumns
}

type humanPasskeyConditions interface {
	PasskeyIDCondition(passkeyID string) database.Condition
	PasskeyKeyIDCondition(keyID string) database.Condition
	PasskeyChallengeCondition(challenge string) database.Condition
	PasskeyTypeCondition(passkeyType PasskeyType) database.Condition
}

type humanPasskeyChanges interface {
	// AddPasskey adds a passkey for the user
	AddPasskey(passkey *Passkey) database.Change
	// UpdatePasskey updates a passkey for the user.
	// The condition must specify which passkey to update, use [humanPasskeyConditions].
	// Use [humanPasskeyFieldChanges] to specify the fields to update.
	UpdatePasskey(condition database.Condition, changes ...database.Change) database.Change
	// RemovePasskey removes a passkey for the user
	// The condition must specify which passkey to update, use [humanPasskeyConditions].
	RemovePasskey(condition database.Condition) database.Change

	passkeyFieldChanges
}

type passkeyFieldChanges interface {
	// SetPasskeyKeyID sets the key ID field
	SetPasskeyKeyID(keyID []byte) database.Change
	// SetPasskeyPublicKey sets the public key field
	SetPasskeyPublicKey(publicKey []byte) database.Change
	// SetPasskeyAttestationType sets the attestation type field
	SetPasskeyAttestationType(attestationType string) database.Change
	// SetPasskeyAuthenticatorAttestationGUID sets the AAGUID field
	SetPasskeyAuthenticatorAttestationGUID(aaguid []byte) database.Change
	// SetPasskeyName sets the name field
	SetPasskeyName(name string) database.Change
	// SetPasskeySignCount sets the sign count field
	SetPasskeySignCount(signCount uint32) database.Change
	// SetPasskeyUpdatedAt sets the updated at field
	SetPasskeyUpdatedAt(updatedAt time.Time) database.Change
	// SetPasskeyVerifiedAt sets the verified at field
	// If verifiedAt is zero, it will be set to NOW()
	SetPasskeyVerifiedAt(verifiedAt time.Time) database.Change
}
